#!/usr/bin/env python

# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007 by Infiscape Corporation
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

# The following code comes from the ASPN Python Cookbook. More information is
# available at this address:
#
#    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731

"""Disk And Execution MONitor (Daemon)

Configurable daemon behaviors:

   1.) The current working directory set to the "/" directory.
   2.) The current file creation mode mask set to 0.
   3.) Close all open files (1024). 
   4.) Redirect standard I/O streams to "/dev/null".

A failed call to fork() now raises an exception.

References:
   1) Advanced Programming in the Unix Environment: W. Richard Stevens
   2) Unix Programming Frequently Asked Questions:
         http://www.erlenstar.demon.co.uk/unix/faq_toc.html
"""

__author__ = "Chad J. Schroeder"
__copyright__ = "Copyright (C) 2005 Chad J. Schroeder"

__revision__ = "$Id$"
__version__ = "0.2"

# Standard Python modules.
import os               # Miscellaneous OS interfaces.
import sys              # System-specific parameters and functions.

# Default daemon parameters.
# File mode creation mask of the daemon.
UMASK = 0133

# Default working directory for the daemon.
WORKDIR = "/"

# Default maximum for the number of available file descriptors.
MAXFD = 1024

# The standard I/O file descriptors are redirected to /dev/null by default.
if hasattr(os, "devnull"):
   REDIRECT_TO = os.devnull
else:
   REDIRECT_TO = "/dev/null"

def createDaemon():
   """Detach a process from the controlling terminal and run it in the
   background as a daemon.
   """

   try:
      # Fork a child process so the parent can exit.  This returns control to
      # the command-line or shell.  It also guarantees that the child will not
      # be a process group leader, since the child receives a new process ID
      # and inherits the parent's process group ID.  This step is required
      # to insure that the next call to os.setsid is successful.
      pid = os.fork()
   except OSError, e:
      raise Exception, "%s [%d]" % (e.strerror, e.errno)

   if pid == 0: # The first child.
      # To become the session leader of this new session and the process group
      # leader of the new process group, we call os.setsid().  The process is
      # also guaranteed not to have a controlling terminal.
      os.setsid()

      # Is ignoring SIGHUP necessary?
      #
      # It's often suggested that the SIGHUP signal should be ignored before
      # the second fork to avoid premature termination of the process.  The
      # reason is that when the first child terminates, all processes, e.g.
      # the second child, in the orphaned group will be sent a SIGHUP.
      #
      # "However, as part of the session management system, there are exactly
      # two cases where SIGHUP is sent on the death of a process:
      #
      #   1) When the process that dies is the session leader of a session that
      #      is attached to a terminal device, SIGHUP is sent to all processes
      #      in the foreground process group of that terminal device.
      #   2) When the death of a process causes a process group to become
      #      orphaned, and one or more processes in the orphaned group are
      #      stopped, then SIGHUP and SIGCONT are sent to all members of the
      #      orphaned group." [2]
      #
      # The first case can be ignored since the child is guaranteed not to have
      # a controlling terminal.  The second case isn't so easy to dismiss.
      # The process group is orphaned when the first child terminates and
      # POSIX.1 requires that every STOPPED process in an orphaned process
      # group be sent a SIGHUP signal followed by a SIGCONT signal.  Since the
      # second child is not STOPPED though, we can safely forego ignoring the
      # SIGHUP signal.  In any case, there are no ill-effects if it is ignored.
      #
      # import signal           # Set handlers for asynchronous events.
      # signal.signal(signal.SIGHUP, signal.SIG_IGN)

      try:
         # Fork a second child and exit immediately to prevent zombies.  This
         # causes the second child process to be orphaned, making the init
         # process responsible for its cleanup.  And, since the first child is
         # a session leader without a controlling terminal, it's possible for
         # it to acquire one by opening a terminal in the future (System V-
         # based systems).  This second fork guarantees that the child is no
         # longer a session leader, preventing the daemon from ever acquiring
         # a controlling terminal.
         pid = os.fork()        # Fork a second child.
      except OSError, e:
         raise Exception, "%s [%d]" % (e.strerror, e.errno)

      if (pid == 0):    # The second child.
         # Since the current working directory may be a mounted filesystem, we
         # avoid the issue of not being able to unmount the filesystem at
         # shutdown time by changing it to the root directory.
         os.chdir(WORKDIR)
         # We probably don't want the file mode creation mask inherited from
         # the parent, so we give the child complete control over permissions.
         os.umask(UMASK)
      else:
         # exit() or _exit()?  See below.
         os._exit(0)    # Exit parent (the first child) of the second child.
   else:
      # exit() or _exit()?
      # _exit is like exit(), but it doesn't call any functions registered
      # with atexit (and on_exit) or any registered signal handlers.  It also
      # closes any open file descriptors.  Using exit() may cause all stdio
      # streams to be flushed twice and any temporary files may be unexpectedly
      # removed.  It's therefore recommended that child branches of a fork()
      # and the parent branch(es) of a daemon use _exit().
      os._exit(0)       # Exit parent of the first child.

   # Close all open file descriptors.  This prevents the child from keeping
   # open any file descriptors inherited from the parent.  There is a variety
   # of methods to accomplish this task.  Three are listed below.
   #
   # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum
   # number of open file descriptors to close.  If it doesn't exists, use
   # the default value (configurable).
   #
   # try:
   #    maxfd = os.sysconf("SC_OPEN_MAX")
   # except (AttributeError, ValueError):
   #    maxfd = MAXFD
   #
   # OR
   #
   # if (os.sysconf_names.has_key("SC_OPEN_MAX")):
   #    maxfd = os.sysconf("SC_OPEN_MAX")
   # else:
   #    maxfd = MAXFD
   #
   # OR
   #
   # Use the getrlimit method to retrieve the maximum file descriptor number
   # that can be opened by this process.  If there is not limit on the
   # resource, use the default value.
   #
   import resource      # Resource usage information.
   maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
   if maxfd == resource.RLIM_INFINITY:
      maxfd = MAXFD
  
   # Iterate through and close all file descriptors.
   for fd in range(0, maxfd):
      try:
         os.close(fd)
      except OSError:   # ERROR, fd wasn't open to begin with (ignored)
         pass

   # Redirect the standard I/O file descriptors to the specified file.  Since
   # the daemon has no controlling terminal, most daemons redirect stdin,
   # stdout, and stderr to /dev/null.  This is done to prevent side-effects
   # from reads and writes to the standard I/O file descriptors.

   # This call to open is guaranteed to return the lowest file descriptor,
   # which will be 0 (stdin), since it was closed above.
   os.open(REDIRECT_TO, os.O_RDWR | os.O_CREAT) # standard input (0)

   # Duplicate standard input to standard output and standard error.
   os.dup2(0, 1)        # standard output (1)
   os.dup2(0, 2)        # standard error (2)

   return 0

# Everything from here down is the vpresentd code.
import os.path
import popen2
import pwd
import re
import signal
import traceback


REDIRECT_TO = "/var/log/vpresentd.log"
LOG_FILE_OBJ = None
gPid = -1

def waitpidRetryOnEINTR(pid, options):
   while True:
      try:
         return os.waitpid(pid, options)
      except OSError, ex:
         if ex.errno == errno.EINTR:
            continue
         else:
            raise

def readlineRetryOnEINTR(handle):
   line = ""
   done = False
   while not done:
      try:
         line = handle.readline()
         done = True
      except IOError, ex:
         if ex.errno == errno.EINTR:
            continue
         else:
            raise

   return line

def readlinesRetryOnEINTR(handle):
   lines = []
   done  = False
   while not done:
      try:
         line = handle.readline()
         if line == "":
            done = True
         else:
            lines.append(line)
      except IOError, ex:
         if ex.errno == errno.EINTR:
            continue
         else:
            raise

   return lines

def changeToUserName(userName):
   try:
      pw_entry = pwd.getpwnam(userName)
      changeToUser(pw_entry[2], pw_entry[3])
   except Exception, ex:
      traceback.print_exc(file = LOG_FILE_OBJ)
      os._exit(5)

def changeToUser(uid, gid):
   # NOTE: os.setgid() must be called first or else we will get an
   # "operation not permitted" error.
   os.setgid(gid)
   os.setuid(uid)

def getUserHome(userName):
   pw_entry = pwd.getpwnam(userName)
   return pw_entry[5]

def getUserXauthFile(userName):
   return os.path.join(getUserHome(userName), ".Xauthority")

def addAuthority(user, xauthCmd, xauthFile):
   """
   Pulls the X authority key from the named file and adds it to the named
   user's .Xauthority file if necessary. A tuple containing the the display
   name (suitable for use as the value of the DISPLAY environment variable)
   and a boolean value indicating whether the user's .Xauthority file had to
   be updated is returned. If this boolean value is True, then it should be
   assumed that the user is logged on to the local workstation, and the
   authority should not be removed later using removeAuthority().
   """
   (temp_stdout, temp_stdin) = popen2.popen2("/bin/hostname")

   # Read the output from /bin/hostname. Protect against EINTR just in case.
   hostname = readlineRetryOnEINTR(temp_stdout).strip()
   temp_stdout.close()
   temp_stdin.close()

   host_str = "%s/unix" % hostname

   # Pull out the system X authority key. It will be the first line of the
   # output from running 'xauth list'.
   (child_stdout, child_stdin) = \
      popen2.popen2("%s -f %s list" % (xauthCmd, xauthFile))

   # Read the output from running the above xauth command. Protect against
   # EINTR just in case.
   line = readlineRetryOnEINTR(child_stdout)
   child_stdout.close()
   child_stdin.close()

   key_str = re.sub("#ffff##", host_str, line)
   display_key_re = re.compile(r"\s*(\S+)\s+(\S+)\s+(\S+)\s*")
   key_match = display_key_re.match(key_str)
   if key_match is None:
      raise Exception("Failed to look up X Authority key value in %s" % xauthFile)

   key = (key_match.group(1), key_match.group(2), key_match.group(3))

   # The next step is to determine if the user's Xauthority file already has
   # the key that we just found. This has to be run as the vpresent user
   # since the owner of the vpresentd process (root) may not have access to
   # that user's files.

   # Create the child process.
   pid = os.fork()
   if pid == 0:
      try:
         # Run the xauth(1) command as the vpresent user.
         changeToUserName(user)

         has_key = 0
         user_xauth_file = getUserXauthFile(user)

         try:
            (child_stdout, child_stdin) = \
               popen2.popen2("%s -f %s list" % (xauthCmd, user_xauth_file))

            # Read the contents of the user's X authority file. This is not
            # done using readlines() because that could fail due to an
            # interrupted system call. Instead, we read lines one at a time
            # and handle EINTR if an when it occurs.
            lines = readlinesRetryOnEINTR(child_stdout)
            child_stdout.close()
            child_stdin.close()

            # Determine if the user's Xauthority file already has the key.
            for l in lines:
               key_match = display_key_re.match(l)
               if key_match is not None:
                  user_key = (key_match.group(1), key_match.group(2),
                              key_match.group(3))
                  if user_key == key:
                     has_key = 1
                     break
         # If we fail to determine if the user's .Xauthority file already has
         # the X11 server key, we will go ahead and attempt to add it.
         except Exception, ex:
            print "WARNING: Could not check '%s' for X11 key value: %s" % \
                     (user_xauth_file, str(ex))

         try:
            # If the user's Xauthority file does not have the key, then we add
            # it.
            if not has_key:
               result = -1
               count  = 0
               while result != 0 and count < 10:
                  result = os.spawnl(os.P_WAIT, xauthCmd, xauthCmd, "-f",
                                     user_xauth_file, "add", key[0], key[1],
                                     key[2])
                  count += 1
         except Exception, ex:
            print "ERROR: Failed to extend '%s' with X11 server key: %s" % \
                     (user_xauth_file, str(ex))
            traceback.print_exc(file = LOG_FILE_OBJ)

         # And that's it for us! It is critical that os._exit() be used here
         # rather than sys.exit() in order to prevent a SystemExit exception
         # from being thrown.
         os._exit(has_key)
      except:
         traceback.print_exc(file = LOG_FILE_OBJ)
         os._exit(127)

   # Wait on the child to complete. This returns a tuple containing the
   # process ID and the process exit code. The exit code of the child
   # indicates whether the vpresent user already has the Xauthority key
   # (0 -> False, 1 -> True).
   child_result = waitpidRetryOnEINTR(pid, 0)
   assert child_result[0] == pid

   # child_result[1] is a 16-bit value. The high byte is the exit code of the
   # child process. Valid exit codes are 1 (the user has the X authority key
   # already) and 0 (the user does not have the X authority key). Anything
   # else indicates an error.
   child_exit = child_result[1] >> 8

   if child_exit != 1 and child_exit != 0:
      raise Exception("X Authority granting process failed: %d" % child_exit)

   return (key[0], bool(child_exit))

def removeAuthority(user, xauthCmd, displayName):
   """
   Removes the named display from the given user's .Xauthority file.
   """
   pid = os.fork()
   if pid == 0:
      try:
         # Run the xauth(1) command as the named user.
         changeToUserName(user)
         os.execl(xauthCmd, xauthCmd, "-f", getUserXauthFile(user), "remove",
                  displayName)
      except:
         traceback.print_exc(file = LOG_FILE_OBJ)
         os._exit(127)

   # Wait on the child to complete.
   waitpidRetryOnEINTR(pid, 0)

def handler(signum, frame):
   if gPid != -1:
      os.kill(gPid, signum)
      # Just to be sure that we do not leave an orphaned process hanging
      # around.
      os.waitpid(gPid, 0)

   # If we got a hang-up signal, do not exit. Instead, restore this handler
   # for SIGHUP and continue executing. In other words, we treat a hang-up
   # signal as a restart signal.
   if signum == signal.SIGHUP:
      signal.signal(signal.SIGHUP, handler)
   else:
      os._exit(signum)

def runServer(user, displayName, serverCmd):
   """
   Runs the given server command as the named user. Doing this requires root
   privileges. The command must be the full path to vpresent-server.
   """
   pid = os.fork()
   if pid == 0:
      try:
         changeToUserName(user)

         os.environ["DISPLAY"]    = displayName
         os.environ["XAUTHORITY"] = getUserXauthFile(user)

         os.execl(serverCmd, serverCmd, "--exit-on-error")
      except:
         traceback.print_exc(file = LOG_FILE_OBJ)
         os._exit(127)

   return pid

if __name__ == "__main__":
   ret_code = createDaemon()

   pidfile = file("/var/run/vpresentd.pid", "w")
   pidfile.write(str(os.getpid()) + "\n")
   pidfile.close()

   # createDaemon() closes all open file descriptors and leaves us with one
   # with value 0. (Technically, we also have 1 and 2, but they are just
   # duplicated version of 0.) To be able to get traceback.print_exc() to
   # write into the log file, we need a file object, and that is what this
   # gives us.
   LOG_FILE_OBJ = os.fdopen(0, "a")

   # Set the variables to be used in running vprsent-server below.
   user       = "vpresent"
   server_cmd = "/usr/sbin/vpresent-server"
   xauth_cmd  = "/usr/bin/xauth"
   # RHEL/CentOS/Fedora path when using GDM.
   #xauth_file = "/var/gdm/:0.Xauth"
   # Ubuntu path when using GDM.
   xauth_file = "/var/lib/gdm/:0.Xauth"

   # We run the following infinite loop inside this try/except block so that
   # any exceptions thrown by the code in that loop are caught and printed
   # before we exit. Having the try/except inside the while loop could cause
   # this daemon to start forking prcoesses and bring down the machine.
   try:
      while True:
         display_name, has_key = addAuthority(user, xauth_cmd, xauth_file)

         # Set up our signal handlers. We do this every time so that the code
         # executed above in addAuthority() and below in removeAuthority()
         # does not get screwed up by our special-purpose signal hanlding.
         hup_handler  = signal.signal(signal.SIGHUP, handler)
         int_handler  = signal.signal(signal.SIGINT, handler)
         quit_handler = signal.signal(signal.SIGQUIT, handler)
         term_handler = signal.signal(signal.SIGTERM, handler)

         # We have tighter exception handling here because failures of
         # vpresent-server are acceptable. We simply run the command again
         # the next time around. Failures in this case include the client
         # disconnecting and vpresent-server exiting. This is how we keep the
         # rendering server available (almost) constantly.
         try:
            gPid = runServer(user, display_name, server_cmd)
            os.waitpid(gPid, 0)
         except OSError:
            pass

         # Restore the original signal handlers now that our child process
         # is done.
         signal.signal(signal.SIGHUP, hup_handler)
         signal.signal(signal.SIGINT, int_handler)
         signal.signal(signal.SIGQUIT, quit_handler)
         signal.signal(signal.SIGTERM, term_handler)

         if not has_key:
            removeAuthority(user, xauth_cmd, display_name)
   except:
      traceback.print_exc(file = LOG_FILE_OBJ)

   sys.exit(ret_code)
